Put configuration and secrets in the Config document

Best practice Recommended
#/Config is the standard location for site configuration in Docly: a schema-bound document in the #/ tree, which is never served over HTTP, is editable through a generated form, and is readable from every template and API function. It is two pieces - a schema at #/Schemas/<Name> config.docly defining the fields, and #/Config.docly bound to it. Secrets belong here and nowhere else; use Type "password" so they are masked while editing. Public values such as a Turnstile site key belong here too, so the customer changes everything in one place. Read it with docly.getJson("#/Config"), guarded with || {} because it returns null before the document exists. Some examples use a config global instead - that was undefined in testing while getJson worked, and an unresolved name in a hash expression renders empty rather than failing, so verify before relying on it.

What you'll see

A site needs a value that is not content and not code: an API key, a Turnstile secret, a tracking id, a feed endpoint, a per-environment toggle. It has to be readable by templates and API functions, editable without a deploy, and — for the secret half of it — unreachable from the web. The tempting move is a JSON file somewhere convenient, or a constant in a .hash file. Both are wrong, and the second one publishes the secret.

What's actually happening

#/Config is the standard location for site configuration in Docly. It is a schema-bound document in the #/ tree, which means three things at once: it is never served over HTTP, it is editable through a generated form rather than by hand-editing JSON, and its fields are readable from every template and API function.

This is not one option among several. A workspace that scatters configuration across ad-hoc JSON files has no single place to look, no form to edit it in, and no guarantee about what is exposed.

It is a document with a schema, not a loose file

Two pieces: a schema at #/Schemas/<Name> config.docly defining the fields, and the document itself at #/Config.docly bound to that schema. The schema gives the customer a labelled form with descriptions, marks the required fields, and can use Type: "password" so a secret is not displayed in plain text while editing.

Secrets belong here, and only here

Anything under #/ is unreachable over HTTP — see Keep private data under the # folder. That is what makes #/Config the right home for a secret key, and it is why a secret must never be written into a .hash page, a stylesheet, or a JSON file in a published folder.

Public values live here too. A Turnstile site key is public by design and is written into the markup — but it still belongs in #/Config, so the customer changes both keys in one place rather than hunting through templates.

Reading it

Read the document with docly.getJson("#/Config"). This works in both hash templates and API functions and returns the fields as a plain object.

Some documentation and examples use a config global — #config.CF_Turnstile_Site_Key#. Verified 2026-08-26: that global was undefined in a hash template while docly.getJson("#/Config") returned the fields correctly on the same request. Use getJson, or verify the global with a scratch page before relying on it — an unresolved name in a hash expression renders empty rather than failing, so a wrong guess here silently produces a form with no site key.

What to do

1. The schema — #/Schemas/<Site> config.docly

Ordinary Input fields. Use Type: "password" for secrets and put a real description on each one — the customer will be the one editing this.

{
  "ObjectVariant": "Input",
  "Required": true,
  "Type": "password",
  "Name": "CF_Turnstile_Secret_Key",
  "Field": "Cloudflare Turnstile - secret",
  "Description": "Secret. Read only by #/API/Turnstile.js. Never goes in markup."
}

Name is the data key you read in code; Field is the label the customer sees. Not the other way round.

2. The document — #/Config.docly

{
  "Schema": "<Site> config",
  "Document": {
    "CF_Turnstile_Site_Key":   "0x4AAAAAAA...",
    "CF_Turnstile_Secret_Key": "0x4AAAAAAA..."
  },
  "Tags": [],
  "EmbeddedFiles": []
}

3. Reading it

// In a hash template — public values only
#{ var cfg = docly.getJson("#/Config") || {}; }#
<div class="cf-turnstile" data-sitekey="#docly.htmlAttributeEncode(cfg.CF_Turnstile_Site_Key)#"></div>

// In an API function — where the secret is used
var cfg = docly.getJson("#/Config") || {};
var secret = cfg.CF_Turnstile_Secret_Key;

Always guard with || {}. Before the document exists, getJson returns null, and property access on it throws in an API function and renders empty in a template.

Verify it is not served

curl -s -o /dev/null -w "%{http_code}\n" https://<site>/Config

Must return 404. Run it after creating the document, not from memory.

Checklist

  1. Configuration lives in #/Config — not in ad-hoc JSON files, not as constants in templates.
  2. A schema backs it, so the customer gets a form with labels and descriptions.
  3. Secrets use Type: "password" and are read only where they are used.
  4. Read with docly.getJson("#/Config"), guarded with || {}.
  5. /Config returns 404.